package org.activequant.dao.hibernate;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.activequant.core.domainmodel.InstrumentSpecification;
import org.activequant.core.domainmodel.marketscanner.MarketScan;
import org.activequant.core.domainmodel.marketscanner.MarketScanSeriesSpecification;
import org.activequant.core.domainmodel.marketscanner.ScanCriteria;
import org.activequant.core.domainmodel.marketscanner.ScanDataEntity;
import org.activequant.core.types.TimeStamp;
import org.activequant.dao.IScanDataEntityDao;
import org.activequant.math.algorithms.Interval;
import org.activequant.util.exceptions.DaoException;
import org.activequant.util.tools.ArrayUtils;
import org.apache.log4j.Logger;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Expression;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.springframework.orm.hibernate3.HibernateCallback;

/**
* An abstract class. ScanDataEntityBase&lt;T extends ScanDataEntity&lt;T&gt;&gt; extends DaoSupportBase&lt;T&gt; implements IScanDataEntityDao&lt;T&gt;.<br/>
* Holds the following associated variables:
* <ul>
* <li>clazz(Class&lt;T&gt;)</li>
* <li>hibernateTemplate(HibernateTemplate)</li>
* </ul>
*  @author Dimitar Misev
*/
abstract class ScanDataEntityBase<T extends ScanDataEntity<T>> extends DaoSupportBase<T> implements IScanDataEntityDao<T> {

	Logger log = Logger.getLogger(ScanDataEntityBase.class);
	
	/**
	 * Constructs a ScanDataEntityBase&lt;T extends ScanDataEntity&lt;T&gt;&gt;(extends DaoSupportBase&lt;T&gt; implements IScanDataEntityDao&lt;T&gt;) using the given clazz(Class&lt;T&gt;)
	 * to set its inherited clazz(Class&lt;T&gt;).
	 * @param clazz
	 */
	protected ScanDataEntityBase(Class<T> clazz) {
		super(clazz);
	}
	/**
	 * Constructs a ScanDataEntityBase&lt;T extends MarketDataEntity&lt;T&gt;&gt;(extends DaoSupportBase&lt;T&gt; implements IMarketDataEntityDao&lt;T&gt;) using the given clazz(Class&lt;T&gt;)
	 * to set its inherited clazz(Class&lt;T&gt;).<br/>
	 * The given sessionFactory(SessionFactory) is used to set its Hibernate SessionFactory and create/initialize the inherited hibernateTemplate(HibernateTemplate) for it.
	 * @param clazz
	 * @param sessionFactory
	 */
	protected ScanDataEntityBase(Class<T> clazz, SessionFactory sessionFactory) {
		super(clazz, sessionFactory);
	}
	/**
	 * Finds/returns all persistent instances of the inherited clazz(Class&lt;T&gt;) whose scanCriteria(ScanCriteria) matches the given scanCriteria(ScanCriteria),
	 * ordered by their timeStamp(TimeStamp) (descending)
	 */
	@SuppressWarnings("unchecked")
	public T[] findByScanCriteria(ScanCriteria scanCriteria) throws DaoException {
		// get contract instance
		scanCriteria = lookupIfNeeded(scanCriteria);		
		// find associated candles
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz).add(Expression.eq("scanCriteria", scanCriteria)).addOrder(Order.desc("timeStamp"));	
		List<T> list = getHibernateTemplate().findByCriteria(criteria);		
		return ArrayUtils.asArray(list, clazz);		
	}

	/**
	 * <strong>1.</strong> If the given scanCriteria(ScanCriteria) has a non-null id(Long), it just returns the given scanCriteria(ScanCriteria) without doing anything else.<br/>
	 * <strong>2.</strong> Otherwise, it uses the given scanCriteria(ScanCriteria) as an example to find and return the ScanCriteria.<br/> 
	 * (it must find exactly one, more than one is considered ambiguous and throws an exception)
	 * @param scanCriteria
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private ScanCriteria lookupIfNeeded(final ScanCriteria scanCriteria) {
		if(scanCriteria.hasId()) return scanCriteria; // not needed
		
		return (ScanCriteria)getHibernateTemplate().execute(new HibernateCallback() {
			public Object doInHibernate(Session session) throws HibernateException, SQLException {
				String queryString=ScanCriteriaDao.createScanCriteriaLookupQuery(scanCriteria);
				Query query = session.createQuery(queryString);
				List<ScanCriteria> list= query.list();
				if(list.size()==0){
					//log.warn("No scanCriteria specification for example found ...");
					return null; 
				}else if(list.size() > 1) {
					throw new IllegalArgumentException("ambiguity: scanCriteria matches more than one database entity: " + scanCriteria);
				}
				return list.get(0);
			}
		});
	}
	//public final static TimeStampFormat formatter = new TimeStampFormat("yyyy/MM/dd HH:mm:ss.SSS Z");
	/**
	 * Finds/returns all persistent instances of the inherited clazz(Class&lt;T&gt;) matching the given query(MarketScanSeriesSpecification). That means:<br/>
	 * - Their scanCriteria(ScanCriteria) matches the scanCriteria(ScanCriteria) of the given query(MarketScanSeriesSpecification); and<br/>
	 * - Their timeStamp(TimeStamp) is within the startTimeStamp(TimeStamp) - endTimeStamp(TimeStamp) interval of the given query(MarketScanSeriesSpecification)<br/>
	 * Results are ordered by their timeStamp(TimeStamp) (descending)
	 * 	@param query
	 */
	@SuppressWarnings("unchecked")
	public T[] findByMarketScanSeriesSpecification(final MarketScanSeriesSpecification query) throws DaoException {
		return (T[])getHibernateTemplate().execute(new HibernateCallback() {
			public Object doInHibernate(Session session) throws HibernateException, SQLException {
				ScanCriteria scanCriteria = query.getScanCriteria();
				scanCriteria = lookupIfNeeded(scanCriteria);
				StringBuffer queryString=new StringBuffer("select distinct scan from "+MarketScan.class.getName()+" scan left join fetch scan.rows o where scan.scanCriteria="+scanCriteria.getId()+" ");
				if(query.getStartTimeStamp() != null) {
					queryString.append("and scan.timeStamp>="+query.getStartTimeStamp()+ " ");
				}
				if(query.getEndTimeStamp() != null) {
					queryString.append("and scan.timeStamp<=" +query.getEndTimeStamp()+ " ");
				}
				queryString.append("order by scan.timeStamp desc");
				
				Query query = session.createQuery(queryString.toString());
				List<T> list= query.list();
				if(list.size()==0){
					//log.warn("No instrument specification for example found ...");
					return null; 
				}else{
					return ArrayUtils.asArray(list, clazz);	
				}
//				else if(list.size() > 1) {
//					throw new IllegalArgumentException("ambiguity: instrumentSpecification matches more than one database entity: " + spec);
//				}
//				return list.get(0);
			}
		});
	}
	
	/**
	 * Finds/returns all persistent instances of the inherited clazz(Class&lt;T&gt;) matching the given query(MarketScanSeriesSpecification) who also contain the given instrumentSpecification(InstrumentSpecification). That means:<br/>
	 * - Their scanCriteria(ScanCriteria) matches the scanCriteria(ScanCriteria) of the given query(MarketScanSeriesSpecification); and<br/>
	 * - Their timeStamp(TimeStamp) is within the startTimeStamp(TimeStamp) - endTimeStamp(TimeStamp) interval of the given query(MarketScanSeriesSpecification)<br/>
	 * Results are ordered by their timeStamp(TimeStamp) (descending)
	 * @param query
	 * @param instrumentSpecification
	 */
	@SuppressWarnings("unchecked")
	public T[] findByMarketScanSeriesSpecificationAndInstrumentSpecification(final MarketScanSeriesSpecification query, final InstrumentSpecification instrumentSpecification) throws DaoException {
		return (T[])getHibernateTemplate().execute(new HibernateCallback() {
			public Object doInHibernate(Session session) throws HibernateException, SQLException {
				ScanCriteria scanCriteria = query.getScanCriteria();
				scanCriteria = lookupIfNeeded(scanCriteria);
				StringBuffer queryString=new StringBuffer("select distinct scan from "+MarketScan.class.getName()+" scan left join fetch scan.rows o where scan.scanCriteria="+scanCriteria.getId()+" ");
				if(query.getStartTimeStamp() != null) {
					queryString.append("and scan.timeStamp>="+query.getStartTimeStamp()+ " ");
				}
//				queryString.append("and o.instrumentSpecificationDetails="+instrumentSpecification.getId() + " ");
//				queryString.append("and scan.id in (select parent_id from marketscanrow row where row.instrumentSpecificationDetails="+instrumentSpecification.getId() + ") ");
				if(query.getEndTimeStamp() != null) {
					queryString.append("and scan.timeStamp<=" +query.getEndTimeStamp()+ " ");
				}
				queryString.append("and exists (from scan.rows r where r.instrumentSpecificationDetails="+instrumentSpecification.getId() + ") ");
				queryString.append("order by scan.timeStamp desc");
				
				Query query = session.createQuery(queryString.toString());
				List<T> list= query.list();
				if(list.size()==0){
					//log.warn("No instrument specification for example found ...");
					return null; 
				}else{
					return ArrayUtils.asArray(list, clazz);	
				}
//				else if(list.size() > 1) {
//					throw new IllegalArgumentException("ambiguity: instrumentSpecification matches more than one database entity: " + spec);
//				}
//				return list.get(0);
			}
		});
	}
	/**
	 * Deletes all persistent instances of the inherited clazz(Class&lt;T&gt;)<br/>
	 * (invokes <code>getHibernateTemplate().bulkUpdate("DELETE " + clazz.getName())</code>)
	 */
	@Override
	public void deleteAll() {
		int rows = getHibernateTemplate().bulkUpdate("DELETE " + clazz.getName());
	    log.debug(rows + " rows affected by bulk delete");
	}
	/**
	 * Deletes all persistent instances of the inherited clazz(Class&lt;T&gt;) whose scanCriteria(ScanCriteria) matches the given scanCriteria(ScanCriteria)
	 */
	public void deleteByScanCriteria(ScanCriteria scanCriteria) throws DaoException {
		delete(findByScanCriteria(scanCriteria));
	}
	/**
	 * Deletes all persistent instances of the inherited clazz(Class&lt;T&gt;) matching the given query(MarketScanSeriesSpecification). That means:<br/>
	 * - Their scanCriteria(ScanCriteria) matches the scanCriteria(ScanCriteria) of the given query(MarketScanSeriesSpecification); and<br/>
	 * - Their timeStamp(TimeStamp) is within the startTimeStamp(TimeStamp) - endTimeStamp(TimeStamp) interval of the given query(MarketScanSeriesSpecification)<br/>
	 */
	public void deleteByMarketScanSeriesSpecification(MarketScanSeriesSpecification query) throws DaoException {
		delete(findByMarketScanSeriesSpecification(query));
	}
	/**
	 * Saves or updates the given entity(T). The entity(T extends ScanDataEntity&lt;T&gt;) has a scanCriteria(ScanCriteria), which is also 
	 * saved if it doesn't already exist.
	 */
	@Override
	public T update(T entity) throws DaoException {
		List<T> list = new ArrayList<T>();
		list.add(entity);
		return update(list).get(0);
	}
	/**
	 * Saves or updates each of the given entities(T...). Each entity(T extends MarketDataEntity&lt;T&gt;) has a scanCriteria(ScanCriteria), which is also 
	 * saved if it doesn't already exist. Saves/updates are performed in batches of BATCH_SIZE(100) at a time.<br/>
	 * Then it returns the given entities(T...).
	 */
	@Override
	public T [] update(T... entities) throws DaoException {
		return ArrayUtils.asArray(update(ArrayUtils.asList(entities)), clazz);
	}
	/**
	 * Saves or updates each of the given entities(List&lt;T&gt;). Each entity(T extends ScanDataEntity&lt;T&gt;) has an scanCriteria(ScanCriteria), which is also 
	 * saved if it doesn't already exist. Saves/updates are performed in batches of BATCH_SIZE(100) at a time.<br/>
	 * Then it returns the given entities(List&lt;T&gt;).
	 */
	@SuppressWarnings("unchecked")
	@Override 
	public List<T> update(final List<T> entities) throws DaoException {
		return (List<T>) getHibernateTemplate().execute(new HibernateCallback() {
			public Object doInHibernate(Session session) throws HibernateException, SQLException {		
				ScanCriteria lastSeenScanCriteria = null;
				int count = 0;
				for(T entity : entities) {
					ScanCriteria scanCriteria = entity.getScanCriteria();
					if(!scanCriteria.equals(lastSeenScanCriteria)) {
//						log.debug("looking up " + scanCriteria);
						lastSeenScanCriteria = lookupIfNeeded(scanCriteria);
						//lastSeenSpec = (ScanCriteria) session.createCriteria(spec.getClass()).add(Example.create(spec)).uniqueResult();
						if(lastSeenScanCriteria == null) {
//							log.debug("persisting the scanCriteria");
							session.save(scanCriteria);
							lastSeenScanCriteria = scanCriteria;
						}
					}
					entity.setScanCriteria(lastSeenScanCriteria);
					session.saveOrUpdate(entity);
					if((++count % BATCH_SIZE) == 0) {
//						log.debug("batch update flush at count: " + count);
						session.flush();
						session.clear();
					}
				}
				return entities;
			}
		});
	}
	
	/**
	 * Returns timestamp interval for which database has data available.<br/>
	 * Returns an Interval&lt;TimeStamp&gt; whose from(TimeStamp) and to(TimeStamp) are set with the min and max timeStamp(TimeStamp) of the persistent instances of the inherited clazz(Class&lt;T&gt;) 
	 * matching the given query(SeriesSpecification). That means:<br/>
	 * - Their scanCriteria(ScanCriteria) matches the scanCriteria(ScanCriteria) of the given query(MarketScanSeriesSpecification)<br/>
	 * @param query marketscan series specification
	 * @return timestamp interval, or null if no data.
	 * @throws DaoException
	 */
	@SuppressWarnings("unchecked")
	public Interval<TimeStamp> findAvailableIntervalBySeriesSpecification(MarketScanSeriesSpecification query) throws DaoException {
//		log.debug("looking for interval by a marketscan series specification " + query);
			
		// get contract instance
		ScanCriteria scanCriteria = query.getScanCriteria();
		scanCriteria = lookupIfNeeded(scanCriteria);
			
		// find associated candles
		DetachedCriteria criteria = DetachedCriteria.forClass(clazz).add(Expression.eq("scanCriteria", scanCriteria));
			
		ProjectionList list = Projections.projectionList();
		list.add(Projections.min("timeStamp"));
		list.add(Projections.max("timeStamp"));
		criteria.setProjection(list);
			
//		if(Candle.class.isAssignableFrom(clazz) && query.getTimeFrame() != null) {
//			criteria.add(Expression.eq("timeFrame", query.getTimeFrame()));
//		}
			
		List<T> results = getHibernateTemplate().findByCriteria(criteria);
			
		// Looks like no candles exist, return null
		if(results.size() < 2)
			return null;
			
		return new Interval<TimeStamp>(results.get(0).getTimeStamp(), results.get(1).getTimeStamp());
			
	}

}
